Análise das Redes de Comércio Internacional de Máscaras Cirúrgicas¶

O Impacto da Pandemia COVID-19 nas Dinâmicas Comerciais Globais (2015–2024)¶


Autor: Ricardo Vicente
Mestrado: Análise de Dados e Sistemas de Apoio à Decisão
UC: Otimização em Redes e Redes Sociais
Data: Junho de 2025


Sumário¶

1. Preparação dos dados
 1.1 - Imports e carregamento de dados
 1.2 - Informações básicas dos dados
 1.3 - Info sobre Países
 1.4 - Homogeneizar e corrigir ISO de países
 1.5 - Limpeza de dados e Est. Descritivas
 1.6 - Análise básica de trocas
2. AED
 2.1 - Variações anuais
 2.2 - Top Exportadores em $
 2.3 - Top Exportadores em qty
 2.4 - Modificações nos Tops
3. Construção das redes
 3.1 - Rede Pré-Pandemia - Spring Layout
 3.2 - Análise por Períodos
 3.2.1 - Estatísticas básicas
 3.2.2 - Medidas de Centralidade
 3.2.3 - Propriedades de conectividade
 3.2.4 - Medidas de distância
 3.2.5 - Detecção de Comunidades
 3.3 - Mapas Temporais
4. O Caso de Portugal

1. Preparação dos Dados¶

1.1 Imports e carregamento de dados¶

In [120]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import matplotlib.ticker as ticker
import random
import pycountry
import math
from IPython.display import display
import country_converter as coco
import warnings
warnings.filterwarnings('ignore')

import networkx as nx
import community  
from networkx.algorithms import community as nx_community
import plotly.graph_objects as go

#settings de vis
plt.rcParams['figure.figsize'] = [10, 6]
sns.set_style("whitegrid")
plt.rcParams['font.size'] = 12

# carregar dados
df = pd.read_csv('dataset4.csv', sep=";", encoding='latin-1')

display(df.head())
refYear refMonth flowDesc reporterISO partnerISO qty primaryValue unitPrice
0 2015 1 Import DZA W00 0 391716,54 #DIV/0!
1 2015 1 Import DZA CAN 0 466,05 #DIV/0!
2 2015 1 Import DZA CHN 0 154815 #DIV/0!
3 2015 1 Import DZA CZE 0 54,72 #DIV/0!
4 2015 1 Import DZA FRA 0 47176,61 #DIV/0!

1.2 Informações básicas dos dados¶

In [122]:
# colunas no df
print("Colunas no DataFrame:\n")
print(df.columns.tolist())
print("\n",100*"=")

# infos básicas do dataset
print(f"\nTotal de registos: {len(df)}\n")
print(f"Anos incluídos: {df['refYear'].unique()}")
print(f"Número de países Exportadores: {df['reporterISO'].nunique()}")
print(f"Número de países Importadores: {df['partnerISO'].nunique()}")

print("\n",100*"=")

print("\nTipos de dados:\n")
display(df.dtypes)
Colunas no DataFrame:

['refYear', 'refMonth', 'flowDesc', 'reporterISO', 'partnerISO', 'qty', 'primaryValue', 'unitPrice']

 ====================================================================================================

Total de registos: 794265

Anos incluídos: [2015 2016 2017 2018 2019 2020 2021 2022 2023 2024]
Número de países Exportadores: 156
Número de países Importadores: 247

 ====================================================================================================

Tipos de dados:

refYear          int64
refMonth         int64
flowDesc        object
reporterISO     object
partnerISO      object
qty             object
primaryValue    object
unitPrice       object
dtype: object

1.3 Info sobre Países¶

In [124]:
print(f"Países Exportadores: {df['reporterISO'].unique()}\n")
print(f"\nPaíses Importadores: {df['partnerISO'].unique()}")
Países Exportadores: ['DZA' 'AND' 'AGO' 'ATG' 'AZE' 'ARG' 'AUS' 'AUT' 'BHR' 'ARM' 'BRB' 'BEL'
 'BMU' 'BOL' 'BIH' 'BWA' 'BRA' 'BLZ' 'BRN' 'BGR' 'MMR' 'BDI' 'BLR' 'KHM'
 'CAN' 'CPV' 'CHL' 'COL' 'COM' 'COG' 'COD' 'HRV' 'CYP' 'CZE' 'BEN' 'DNK'
 'DOM' 'ECU' 'SLV' 'ETH' 'EST' 'FJI' 'FIN' 'FRA' 'PYF' 'GEO' 'DEU' 'GHA'
 'GRC' 'GRL' 'GRD' 'GTM' 'GUY' 'HKG' 'HUN' 'ISL' 'IDN' 'IRL' 'ISR' 'ITA'
 'CIV' 'JPN' 'KAZ' 'KOR' 'KWT' 'KGZ' 'LAO' 'LSO' 'LVA' 'LTU' 'LUX' 'MAC'
 'MDG' 'MWI' 'MYS' 'MLT' 'MUS' 'MEX' 'MDA' 'MNE' 'MSR' 'MAR' 'MOZ' 'OMN'
 'NLD' 'NCL' 'NZL' 'NIC' 'NOR' 'PLW' 'PAK' 'PAN' 'PRY' 'PER' 'PHL' 'POL'
 'PRT' 'QAT' 'ROU' 'RUS' 'RWA' 'KNA' 'VCT' 'STP' 'SAU' 'SEN' 'SRB' 'SYC'
 'IND' 'SGP' 'SVK' 'VNM' 'SVN' 'ZAF' 'ZWE' 'ESP' 'SDN' 'SWZ' 'SWE' 'CHE'
 'THA' 'TTO' 'TUR' 'UGA' 'UKR' 'MKD' 'EGY' 'GBR' 'TZA' 'USA' 'BFA' 'URY'
 'WSM' 'YEM' 'ZMB' 'TGO' 'GMB' 'SLB' 'CHN' 'HND' 'MNG' 'NER' 'SLE' 'PSE'
 'KIR' 'JOR' 'KEN' 'NAM' 'ARE' 'CRI' 'MDV' 'NGA' 'ABW' 'UZB' 'MRT' 'TUN']


Países Importadores: ['W00' 'CAN' 'CHN' 'CZE' 'FRA' 'ITA' 'KOR' 'S19' 'PAK' 'POL' 'PRT' 'RUS'
 'IND' 'ESP' 'SWE' 'TUN' 'TUR' 'EGY' 'GBR' 'USA' 'DEU' 'NLD' 'CHE' 'BEL'
 'BRA' 'COD' 'DNK' 'ISR' 'JPN' 'LBN' 'MEX' 'MAR' 'NAM' 'SGP' 'ZAF' 'ARE'
 'SXM' 'PER' 'AUS' 'IRN' 'LKA' 'COL' 'HKG' 'MYS' 'ROU' 'VNM' 'URY' 'AUT'
 'BGD' 'BIH' 'KHM' 'FJI' 'FIN' 'HND' 'HUN' 'IDN' 'IRL' 'KEN' 'LVA' 'MDG'
 'NPL' 'NZL' 'NOR' 'PHL' 'SVK' 'THA' 'UKR' 'MKD' 'BOL' 'HRV' 'EST' 'GMB'
 'LUX' 'SRB' 'SVN' '_X ' 'CRI' 'GIN' 'JAM' 'PAN' 'MAF' 'SUR' 'CHL' 'SAU'
 'ARG' 'GTM' 'BGR' 'ZMB' 'VGB' 'ECU' 'BRB' 'GRC' 'UGA' 'LTU' 'TZA' 'DMA'
 'NIC' 'PRY' 'ATA' 'GHA' 'KWT' 'MLI' 'NCL' 'PNG' 'SEN' 'TON' 'X1 ' 'AZE'
 'BHS' 'ARM' 'BLR' 'CMR' 'CYP' 'GAB' 'GEO' 'ISL' 'KAZ' 'LBR' 'LBY' 'MLT'
 'MNE' 'OMN' 'QAT' 'SYR' 'X2 ' 'CYM' 'GRD' 'LCA' 'VCT' 'TTO' 'DZA' 'BEN'
 'GNQ' 'ERI' 'PYF' 'GIB' 'IRQ' 'JOR' 'MUS' 'F19' 'SLE' 'AGO' 'CIV' 'MOZ'
 'AFG' 'DOM' 'TKM' 'SLV' 'E19' 'SMR' 'LAO' 'MDA' 'NGA' 'ALB' 'BHR' 'BMU'
 'CUB' 'CUW' 'UMI' 'SPM' 'BFA' 'HTI' 'VEN' 'MDV' 'FRO' 'GRL' 'KGZ' 'UZB'
 'SLB' 'VUT' 'ATG' 'BVT' 'PRK' 'GUM' 'AND' 'CAF' 'TCD' 'COM' 'COG' 'DJI'
 'MRT' 'ABW' 'RWA' 'BLM' 'SYC' 'ZWE' 'TGO' 'PSE' 'MAC' 'BRN' 'GUY' 'MNG'
 'SDN' 'YEM' 'MMR' 'BLZ' 'ETH' 'BDI' 'WLF' 'COK' 'PLW' 'WSM' 'SOM' 'CPV'
 'GNB' 'STP' 'LSO' 'XX ' 'BTN' 'MWI' 'NER' 'MNP' 'BWA' 'SWZ' 'TJK' 'MHL'
 'KNA' 'TCA' 'KIR' 'BES' 'A79' 'NIU' 'TLS' 'AIA' 'NRU' 'TKL' 'O19' 'MSR'
 'ASM' 'TUV' 'FSM' 'SSD' 'CXR' 'NFK' 'IOT' 'MYT' 'CCK' 'SHN' 'FLK' 'SGS'
 'VAT' 'ATF' 'PCN' 'ESH' 'ATB' 'A59' 'HMD']

1.4 Homogeneizar e corrigir ISO de países¶

In [126]:
# Todo este bloco de código foi sugerido pelo chatGPT. Utilizei para, dada a lista completa de países, 
# detectar redundâncias, erros ou nomes que não correspondem a países reais. Após fazer isso, sugeriu esta função para corrigir
# nomes. A função estava mais simples que a minha, optei usar esta. 

def corrigir_nomes_paises(df):
    
    df_corrigido = df.copy()
      
    # Códigos que não existem 
    entidades_especiais = ['W00', 'S19', '_X ', 'X1 ', 'X2 ', 'F19', 'E19', 'A79', 'O19', 'A59', 'XX ']
    
    # Filtrar apenas países reais (removendo todas as entidades especiais)

    df_corrigido = df_corrigido[
        (~df_corrigido['reporterISO'].isin(entidades_especiais)) & 
        (~df_corrigido['partnerISO'].isin(entidades_especiais))
    ]
    
    return df_corrigido

df_corrigido = corrigir_nomes_paises(df)
In [127]:
# infos básicas do dataset
print(f"\nTotal de registos: {len(df_corrigido)}\n")
print(f"Anos incluídos: {df_corrigido['refYear'].unique()}")
print(f"Número de países Exportadores: {df_corrigido['reporterISO'].nunique()}")
print(f"Número de países Importadores: {df_corrigido['partnerISO'].nunique()}")
Total de registos: 748813

Anos incluídos: [2015 2016 2017 2018 2020 2021 2022 2023 2024]
Número de países Exportadores: 155
Número de países Importadores: 236

Nota: Ao retirar o WOO como _"entidade_especial" o ano de 2019 é todo eliminado pois todas os importadores de 2019 estão listados como WOO em partnerISO

1.5 Limpeza de dados e Est. Descritivas¶

In [130]:
# limpar #N/A decorrente de qty = 0 no cálculo do unitPrice - está com #N/A porque fiz este cálculo no excel, antes de carregar os dados aqui
df_corrigido['qty'] = df_corrigido['qty'].replace('#N/A', np.nan)

# conversão de formatos numéricos
df_corrigido['primaryValue'] = df_corrigido['primaryValue'].astype(str).str.replace(',', '.', regex=False)
df_corrigido['qty'] = df_corrigido['qty'].astype(str).str.replace(',', '.', regex=False)
df_corrigido['primaryValue'] = pd.to_numeric(df_corrigido['primaryValue'], errors='coerce')
df_corrigido['qty'] = pd.to_numeric(df_corrigido['qty'], errors='coerce')

print("\nValores ausentes por coluna:\n")
print(df_corrigido.isnull().sum())

# criar colunas para países, há nomes que precisam de ser homogeneizados 
df_corrigido['Exportador'] = df_corrigido['reporterISO']
df_corrigido['Importador'] = df_corrigido['partnerISO']

# criar df filtrado apenas com exportações
print("Valores únicos na coluna flowDesc:")
print(df_corrigido['flowDesc'].unique())

df_exports = df_corrigido[df_corrigido['flowDesc'] == 'Export'].copy()
print(f"\nTotal de registos de exportação: {len(df_exports)}\n")

# verificar valores ausentes
print("\nValores ausentes após conversão:\n")
print(df_exports[['primaryValue', 'qty']].isna().sum())

# resumo
print("\nResumo estatístico após limpeza:")
stats = df_exports[['primaryValue', 'qty']].describe().round(2)
html_stats = stats.style.format("{:,.2f}")
display(html_stats)
Valores ausentes por coluna:

refYear         0
refMonth        0
flowDesc        0
reporterISO     0
partnerISO      0
qty             0
primaryValue    0
unitPrice       0
dtype: int64
Valores únicos na coluna flowDesc:
['Import' 'Export']

Total de registos de exportação: 349622


Valores ausentes após conversão:

primaryValue    0
qty             0
dtype: int64

Resumo estatístico após limpeza:
  primaryValue qty
count 349,622.00 349,622.00
mean 472,476.06 34,380.18
std 12,406,529.79 577,240.69
min 0.00 0.00
25% 869.00 8.90
50% 7,749.73 200.00
75% 62,773.85 2,870.99
max 3,653,069,488.00 77,478,428.00

1.6 Análise básica de trocas¶

In [132]:
# versão agregada por ano (soma dos valores)
df_export_annual = df_exports.groupby(['refYear', 'reporterISO', 'partnerISO'])['primaryValue'].sum().reset_index()

# dados de exportação agregados
print("\nDados de exportação agregados por ano:")
display(df_export_annual.head())

# principais países exportadores e importadores
top_exporters = df_export_annual.groupby('reporterISO')['primaryValue'].sum().sort_values(ascending=False).head(10)
top_importers = df_export_annual.groupby('partnerISO')['primaryValue'].sum().sort_values(ascending=False).head(10)

print("\nTop 10 países exportadores:")

display(top_exporters)

print("\nTop 10 países importadores:")
display(top_importers)
Dados de exportação agregados por ano:
refYear reporterISO partnerISO primaryValue
0 2015 AGO ARE 188.12
1 2015 AGO BEL 3347.41
2 2015 AGO BRA 49.50
3 2015 AGO COD 157.44
4 2015 AGO COG 45907.17
Top 10 países exportadores:
reporterISO
CHN    9.582094e+10
DEU    9.595403e+09
USA    7.148129e+09
MEX    5.745379e+09
VNM    5.534274e+09
IND    3.760615e+09
NLD    3.366292e+09
POL    2.576971e+09
FRA    2.408748e+09
TUR    2.206861e+09
Name: primaryValue, dtype: float64
Top 10 países importadores:
partnerISO
USA    4.319084e+10
DEU    1.379284e+10
JPN    1.092044e+10
FRA    9.253455e+09
GBR    8.129058e+09
ITA    5.287905e+09
CAN    5.286796e+09
ESP    4.798290e+09
NLD    4.716897e+09
MEX    3.948564e+09
Name: primaryValue, dtype: float64

2. AED¶

2.1 Variações anuais¶

In [135]:
# análise temporal do valor total de exportações por ano
yearly_stats = df_exports.groupby('refYear').agg({
    'primaryValue': 'sum',
    'qty': 'sum',
    'Exportador': 'nunique',
    'Importador': 'nunique'
}).reset_index()

# preço médio por unidade para cada ano
yearly_stats['Preço Médio por Unidade (USD)'] = yearly_stats['primaryValue'] / yearly_stats['qty']

# renomear colunas
yearly_stats = yearly_stats.rename(columns={
    'primaryValue': 'Valor Total (USD)',
    'qty': 'Quantidade Total',
    'Exportador': 'Número de Exportadores',
    'Importador': 'Número de Importadores'
})

fig, axes = plt.subplots(2, 2, figsize=(16, 12))

# mil milhões = 1e9 
# Grafico 1: Valor Total das Exportações
axes[0, 0].bar(yearly_stats['refYear'], yearly_stats['Valor Total (USD)'] / 1e9, color='steelblue')
axes[0, 0].set_title('Valor Total das Exportações de Máscaras')
axes[0, 0].set_xlabel('Ano')
axes[0, 0].set_ylabel('mil Milhões USD')
axes[0, 0].grid(axis='y', linestyle='--', alpha=0.7)
# Adicionar rótulos de valor
for i, value in enumerate(yearly_stats['Valor Total (USD)']):
    axes[0, 0].text(yearly_stats['refYear'].iloc[i], value/1e9 + 2, f'${value/1e9:.1f}mM', 
                 ha='center', va='bottom')

# Grafico 2: Quantidade Total de Máscaras
axes[0, 1].bar(yearly_stats['refYear'], yearly_stats['Quantidade Total'] / 1e9, color='darkgreen')
axes[0, 1].set_title('Quantidade Total de Máscaras Exportadas')
axes[0, 1].set_xlabel('Ano')
axes[0, 1].set_ylabel('mil Milhões de Unidades')
axes[0, 1].grid(axis='y', linestyle='--', alpha=0.7)
# Adicionar rótulos de valor
for i, value in enumerate(yearly_stats['Quantidade Total']):
    axes[0, 1].text(yearly_stats['refYear'].iloc[i], value/1e9 + 0.2, f'{value/1e9:.1f}mM', 
                 ha='center', va='bottom')

# Grafico 3: Preço Médio por Unidade
axes[1, 0].bar(yearly_stats['refYear'], yearly_stats['Preço Médio por Unidade (USD)'], color='purple')
axes[1, 0].set_title('Preço Médio por Unidade de Máscara')
axes[1, 0].set_xlabel('Ano')
axes[1, 0].set_ylabel('USD')
axes[1, 0].grid(axis='y', linestyle='--', alpha=0.7)
# Adicionar rótulos de valor
for i, value in enumerate(yearly_stats['Preço Médio por Unidade (USD)']):
    axes[1, 0].text(yearly_stats['refYear'].iloc[i], value + 1, f'${value:.2f}', 
                 ha='center', va='bottom')

# Grafico 4: Número de Países Exportadores 
axes[1, 1].plot(yearly_stats['refYear'], yearly_stats['Número de Exportadores'], 'o-', color='blue', label='Exportadores')

importers_adjusted = yearly_stats['Número de Importadores'].copy()
importers_adjusted.loc[yearly_stats['refYear'] == 2019]  # Marcar 2019 como dados ausentes

mask = yearly_stats['refYear'] != 2019
axes[1, 1].plot(yearly_stats.loc[mask, 'refYear'], importers_adjusted[mask], 's-', color='red', label='Importadores')
axes[1, 1].set_title('Número de Países Envolvidos no Comércio')
axes[1, 1].set_xlabel('Ano')
axes[1, 1].set_ylabel('Número de Países')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)
# adicionar nota sobre o ano 2019
axes[1, 1].annotate('Dados de 2019 apenas\nagregados como "World"', 
                  xy=(2019, 100), xytext=(2019, 150),
                  arrowprops=dict(facecolor='black', shrink=0.05, width=1.5),
                  ha='center', fontsize=10)

plt.tight_layout()
plt.show()


print("\nComparação de Períodos:")
# tirar 2019 da análise pré-pandemia devido à limitação dos dados
pre_pandemia_ajustado = yearly_stats[yearly_stats['refYear'] < 2019]
durante_pandemia = yearly_stats[(yearly_stats['refYear'] >= 2020) & (yearly_stats['refYear'] <= 2021)]
pos_pandemia = yearly_stats[yearly_stats['refYear'] > 2021]

print(f"Média de Valor Total Pré-Pandemia (2015-2019): ${pre_pandemia_ajustado['Valor Total (USD)'].mean()/1e9:.2f} mil Milhões")
print(f"Média de Valor Total Durante Pandemia (2020-2021): ${durante_pandemia['Valor Total (USD)'].mean()/1e9:.2f} mil Milhões")
print(f"Média de Valor Total Pós-Pandemia (2022-2024): ${pos_pandemia['Valor Total (USD)'].mean()/1e9:.2f} mil Milhões")

print(f"\nMédia de Preço por Unidade Pré-Pandemia (2015-2019): ${pre_pandemia_ajustado['Preço Médio por Unidade (USD)'].mean():.2f}")
print(f"Média de Preço por Unidade Durante Pandemia (2020-2021): ${durante_pandemia['Preço Médio por Unidade (USD)'].mean():.2f}")
print(f"Média de Preço por Unidade Pós-Pandemia (2022-2024): ${pos_pandemia['Preço Médio por Unidade (USD)'].mean():.2f}")
No description has been provided for this image
Comparação de Períodos:
Média de Valor Total Pré-Pandemia (2015-2019): $9.23 mil Milhões
Média de Valor Total Durante Pandemia (2020-2021): $45.43 mil Milhões
Média de Valor Total Pós-Pandemia (2022-2024): $12.47 mil Milhões

Média de Preço por Unidade Pré-Pandemia (2015-2019): $11.41
Média de Preço por Unidade Durante Pandemia (2020-2021): $18.72
Média de Preço por Unidade Pós-Pandemia (2022-2024): $9.78

2.2 Top Exportadores em $¶

In [137]:
periodos = {
    "Pré-Pandemia (2015-2018)": df_exports[df_exports['refYear'] < 2019],
    "Durante a Pandemia (2020-2021)": df_exports[(df_exports['refYear'] >= 2020) & (df_exports['refYear'] <= 2021)],
    "Pós-Pandemia (2022-2024)": df_exports[df_exports['refYear'] > 2021]
}

#função para top exporters
def top_exportadores(df, n=10):
    return (
        df.groupby('Exportador')['primaryValue'].sum()
        .nlargest(n)
        .reset_index()
    )


def plot_top(df_top, titulo):
    df_top = df_top.sort_values('primaryValue')
    plt.figure(figsize=(10, 6))
    bars = plt.barh(df_top['Exportador'], df_top['primaryValue'] / 1e9, color='steelblue')
    plt.xlabel('Exportações (mil Milhões USD)')
    plt.title(titulo)
    plt.grid(axis='x', linestyle='--', alpha=0.6)
    for bar in bars:
        plt.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height()/2, f'{bar.get_width():.1f}mM', va='center')
    plt.tight_layout()
    plt.show()

# amrazenar resultados e mostrar
tops = {}
for nome, dados in periodos.items():
    tops[nome] = top_exportadores(dados)
    print(f"\nTop 10 Exportadores - {nome}")
    for i, row in tops[nome].iterrows():
        print(f"{i+1}. {row['Exportador']}: ${row['primaryValue'] / 1e9:.2f}mM")
    plot_top(tops[nome], f'Top 10 Exportadores de Máscaras - {nome}')
Top 10 Exportadores - Pré-Pandemia (2015-2018)
1. CHN: $14.06mM
2. DEU: $3.05mM
3. USA: $2.66mM
4. VNM: $2.09mM
5. MEX: $2.02mM
6. IND: $2.00mM
7. NLD: $0.98mM
8. FRA: $0.74mM
9. GBR: $0.68mM
10. HKG: $0.67mM
No description has been provided for this image
Top 10 Exportadores - Durante a Pandemia (2020-2021)
1. CHN: $66.26mM
2. DEU: $3.42mM
3. VNM: $2.49mM
4. USA: $2.11mM
5. MEX: $1.48mM
6. NLD: $1.23mM
7. HKG: $1.00mM
8. KOR: $0.99mM
9. TUR: $0.97mM
10. FRA: $0.92mM
No description has been provided for this image
Top 10 Exportadores - Pós-Pandemia (2022-2024)
1. CHN: $15.50mM
2. DEU: $3.13mM
3. USA: $2.38mM
4. MEX: $2.25mM
5. NLD: $1.16mM
6. POL: $1.12mM
7. IND: $1.04mM
8. VNM: $0.95mM
9. TUR: $0.79mM
10. FRA: $0.76mM
No description has been provided for this image

2.3 Top Exportadores em qty¶

In [139]:
periodos = {
    "Pré-Pandemia (2015-2018)": df_exports[df_exports['refYear'] < 2019],
    "Durante a Pandemia (2020-2021)": df_exports[(df_exports['refYear'] >= 2020) & (df_exports['refYear'] <= 2021)],
    "Pós-Pandemia (2022-2024)": df_exports[df_exports['refYear'] > 2021]
}

#função para top exporters
def top_exportadores(df, n=10):
    return (
        df.groupby('Exportador')['qty'].sum()
        .nlargest(n)
        .reset_index()
    )

def plot_top(df_top, titulo):
    df_top = df_top.copy()
    df_top['qty'] = df_top['qty'] / 1000
    df_top = df_top.sort_values('qty')

    plt.figure(figsize=(10, 6))
    bars = plt.barh(df_top['Exportador'], df_top['qty'], color='steelblue')
    plt.xlabel('Exportações (toneladas)')
    plt.title(titulo)
    plt.grid(axis='x', linestyle='--', alpha=0.6)
    for bar in bars:
        plt.text(bar.get_width() + 0.1, bar.get_y() + bar.get_height()/2, f'{bar.get_width():.1f} t', va='center')
    plt.tight_layout()
    plt.show()

# armazenar resultados e mostrar
tops = {}
for nome, dados in periodos.items():
    tops[nome] = top_exportadores(dados)
    print(f"\nTop 10 Exportadores - {nome}")
    for i, row in tops[nome].iterrows():
        print(f"{i+1}. {row['Exportador']}: {row['qty'] / 1000:.2f} toneladas")
    plot_top(tops[nome], f'Top 10 Exportadores de Máscaras - {nome}')
Top 10 Exportadores - Pré-Pandemia (2015-2018)
1. CHN: 1370895.94 toneladas
2. HKG: 771349.28 toneladas
3. DEU: 207818.22 toneladas
4. NLD: 126099.65 toneladas
5. VNM: 116922.12 toneladas
6. MEX: 112478.94 toneladas
7. IND: 107670.14 toneladas
8. TUR: 55077.03 toneladas
9. USA: 47960.28 toneladas
10. THA: 33598.85 toneladas
No description has been provided for this image
Top 10 Exportadores - Durante a Pandemia (2020-2021)
1. CHN: 3302422.32 toneladas
2. DEU: 165005.51 toneladas
3. USA: 160471.86 toneladas
4. NLD: 123349.83 toneladas
5. VNM: 100976.72 toneladas
6. TUR: 77638.38 toneladas
7. POL: 64099.71 toneladas
8. IND: 60624.55 toneladas
9. MEX: 59482.16 toneladas
10. HKG: 44607.69 toneladas
No description has been provided for this image
Top 10 Exportadores - Pós-Pandemia (2022-2024)
1. CHN: 2119386.72 toneladas
2. NLD: 236143.53 toneladas
3. USA: 200440.61 toneladas
4. DEU: 188046.24 toneladas
5. MEX: 132135.50 toneladas
6. TUR: 101381.22 toneladas
7. IND: 98644.41 toneladas
8. POL: 85724.99 toneladas
9. VNM: 61756.26 toneladas
10. ESP: 53605.98 toneladas
No description has been provided for this image

2.4 Modificações nos Tops¶

In [141]:
# comparar rankings entre períodos
def comparar_rankings(top_a, top_b, nome_a, nome_b):
    set_a, set_b = set(top_a['Exportador']), set(top_b['Exportador'])
    novos = set_b - set_a
    sairam = set_a - set_b
    comuns = set_a & set_b

    print(f"\n Mudanças do Top 10 entre {nome_a} e {nome_b}:")
    if novos:
        print(f" Entraram no top 10 em {nome_b}: {', '.join(novos)}")
    if sairam:
        print(f" Saíram do top 10 em {nome_b}: {', '.join(sairam)}")
    
    # análise de variação de posição
    rank_a = {c: i+1 for i, c in enumerate(top_a['Exportador'])}
    rank_b = {c: i+1 for i, c in enumerate(top_b['Exportador'])}
    mudancas = [(c, rank_a[c], rank_b[c], rank_a[c] - rank_b[c]) for c in comuns]
    mudancas.sort(key=lambda x: abs(x[3]), reverse=True)

    print("\n Maiores mudanças de posição:")
    for c, r1, r2, delta in mudancas[:5]:
        direcao = "subiu" if delta > 0 else "desceu"
        print(f"{c}: {r1}º -> {r2}º ({direcao} {abs(delta)} posições)")

comparar_rankings(tops["Pré-Pandemia (2015-2018)"], tops["Durante a Pandemia (2020-2021)"],
                  "Pré-Pandemia", "Durante a Pandemia")
comparar_rankings(tops["Durante a Pandemia (2020-2021)"], tops["Pós-Pandemia (2022-2024)"],
                  "Durante a Pandemia", "Pós-Pandemia")
 Mudanças do Top 10 entre Pré-Pandemia e Durante a Pandemia:
 Entraram no top 10 em Durante a Pandemia: POL
 Saíram do top 10 em Durante a Pandemia: THA

 Maiores mudanças de posição:
HKG: 2º -> 10º (desceu 8 posições)
USA: 9º -> 3º (subiu 6 posições)
MEX: 6º -> 9º (desceu 3 posições)
TUR: 8º -> 6º (subiu 2 posições)
DEU: 3º -> 2º (subiu 1 posições)

 Mudanças do Top 10 entre Durante a Pandemia e Pós-Pandemia:
 Entraram no top 10 em Pós-Pandemia: ESP
 Saíram do top 10 em Pós-Pandemia: HKG

 Maiores mudanças de posição:
VNM: 5º -> 9º (desceu 4 posições)
MEX: 9º -> 5º (subiu 4 posições)
NLD: 4º -> 2º (subiu 2 posições)
DEU: 2º -> 4º (desceu 2 posições)
IND: 8º -> 7º (subiu 1 posições)

3. Construção das redes¶

In [143]:
# função para criar redes por periodo
def create_network_period(df, year, threshold=0):
    
    G = nx.DiGraph(name=f"Rede de Exportação de Máscaras Cirúrgicas - {year}")
    
    # Agrupar total de qty por par de países
    grouped = df.groupby(['Exportador', 'Importador'])['qty'].sum().reset_index()

    for _, row in grouped.iterrows():
        if row['qty'] > threshold:
            G.add_edge(row['Exportador'],
                row['Importador'],
                weight=row['qty']
            )
    
    return G


#função para obter posições geográficas
def get_geo_positions(G):
    pos = {}
    for node in G.nodes():
        if node in coords:
            pos[node] = (coords[node]['Longitude'], coords[node]['Latitude'])
    return pos
In [144]:
all_qty = pd.concat([
    periodos["Pré-Pandemia (2015-2018)"]['qty'],
    periodos["Durante a Pandemia (2020-2021)"]['qty'],
    periodos["Pós-Pandemia (2022-2024)"]['qty']
])

print("Resumo estatístico dos pesos (qty):")
print(all_qty.describe(percentiles=[.25, .5, .75, .9, .95, .99]))
Resumo estatístico dos pesos (qty):
count    3.496220e+05
mean     3.438018e+04
std      5.772407e+05
min      0.000000e+00
25%      8.900000e+00
50%      2.000000e+02
75%      2.870992e+03
90%      2.148596e+04
95%      6.436371e+04
99%      5.055140e+05
max      7.747843e+07
Name: qty, dtype: float64

Testar valores para threshold¶

In [146]:
for nome, df in periodos.items():
    n_total = df.groupby(['Exportador', 'Importador']).ngroups
    n_filtrado = df[df['qty'] > 60000].groupby(['Exportador', 'Importador']).ngroups
    print(f"{nome}: Total = {n_total}, Após threshold = {n_filtrado}, % mantido = {n_filtrado / n_total:.2%}")
Pré-Pandemia (2015-2018): Total = 8381, Após threshold = 396, % mantido = 4.72%
Durante a Pandemia (2020-2021): Total = 8067, Após threshold = 609, % mantido = 7.55%
Pós-Pandemia (2022-2024): Total = 8111, Após threshold = 553, % mantido = 6.82%

Ler ficheiro com coordenadas e montar redes¶

In [148]:
# dataFrame de coordenadas dos países
coords_df = pd.read_csv('in-nodes-All.csv')  
coords = coords_df.set_index('Label')[['Longitude', 'Latitude']].to_dict('index')
# montar redes agregadas por período 
networks_periodos = {
    "Pré-Pandemia": create_network_period(periodos["Pré-Pandemia (2015-2018)"], "Pré", threshold=60000),
    "Durante Pandemia": create_network_period(periodos["Durante a Pandemia (2020-2021)"], "Durante", threshold=60000),
    "Pós-Pandemia": create_network_period(periodos["Pós-Pandemia (2022-2024)"], "Pós", threshold=60000),
}

3.1 Rede Pré-Pandemia - Spring Layout¶

In [150]:
G = create_network_period(periodos["Pré-Pandemia (2015-2018)"], "Pré", threshold=2000)
#spring layout
pos = nx.spring_layout(G)

plt.figure(figsize=(12, 8))
nx.draw(G, pos, node_color='lightblue',node_size=50,edge_color='gray',alpha=0.6,with_labels=False,arrows=True,arrowsize=10)
plt.title("Rede de Exportações - Pré-Pandemia")
plt.show()
No description has been provided for this image

3.2 Análise por Períodos - Com coordenadas Geo¶

In [152]:
# desenhar redes
for title, G in networks_periodos.items():
    plt.figure(figsize=(15, 10))
    pos = get_geo_positions(G)
    
    # desenhar nós 
    nodes = list(pos.keys())
    node_sizes = [G.degree(n)*10 for n in nodes]
    node_color='b'
    edge_width = 0.5
    nx.draw_networkx_labels(G, pos, font_size=14, font_color='w')
    nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color=node_color, alpha=0.7)
    nx.draw_networkx_edges(G, pos, connectionstyle='arc3,rad=0.2', edge_color='green', width=edge_width, arrowstyle='->', arrowsize=25)
    
    plt.title(f"Rede de Exportação de Máscaras Cirúrgicas - {title}", fontsize=16)
    plt.xticks([])
    plt.yticks([])
    plt.xlim(-180, 180)
    plt.ylim(-90, 90)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image
In [153]:
# desenhar redes sem labels e apenas ocm uma amostra das arestas
for title, G in networks_periodos.items():
    pos = get_geo_positions(G)
    
    # filtrar arestas para reduzir densidade, 800 escolhidas de forma aleatória por agora
    all_edges = list(G.edges())
    sample_size = min(800, len(all_edges))  
    edges_sample = random.sample(all_edges, sample_size)
    
    # desenhar nós
    nodes = list(pos.keys())
    node_sizes = [min(G.degree(n)*2, 100) for n in nodes] 
    node_color='b'
    edge_width = 0.5
    
    plt.figure(figsize=(15, 10))
    
    nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color=node_color, alpha=0.8)
    nx.draw_networkx_edges(G, pos, edgelist=edges_sample, connectionstyle='arc3,rad=0.2', 
                           edge_color='green', width=edge_width, arrowstyle='->', arrowsize=25, alpha=0.6)
    
    plt.title(f"Rede de Exportação de Máscaras Cirúrgicas - {title}", fontsize=16)
    plt.xticks([])
    plt.yticks([])
    plt.xlim(-180, 180)
    plt.ylim(-90, 90)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

Inserir Mapa¶

In [155]:
import cartopy.crs as ccrs
import cartopy.feature as cfeature

for title, G in networks_periodos.items():
    pos = get_geo_positions(G)
    
    all_edges = list(G.edges())
    sample_size = min(800, len(all_edges))
    edges_sample = random.sample(all_edges, sample_size)
    
    nodes = list(pos.keys())
    node_sizes = [min(G.degree(n)*2, 100) for n in nodes]
    
    fig = plt.figure(figsize=(15, 10))
    ax = plt.axes(projection=ccrs.PlateCarree())
    
    ax.add_feature(cfeature.LAND, facecolor='lightgray')
    ax.add_feature(cfeature.OCEAN, facecolor='lightblue')
    ax.add_feature(cfeature.COASTLINE, linewidth=0.5)
    ax.add_feature(cfeature.BORDERS, linewidth=0.5, alpha=0.5)
    
    nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color='b', alpha=0.5,ax=ax)
    nx.draw_networkx_edges(G, pos, edgelist=edges_sample, edge_color='darkgreen', width=0.5, alpha=0.4,arrows=True, arrowsize=5, connectionstyle='arc3,rad=0.1', ax=ax)
    
    ax.set_global()
    
    plt.title(f"Rede de Exportação de Máscaras Cirúrgicas - {title}", fontsize=16)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

filtrar top 10% das arestas com maior peso e eliminar a seleção aleatório das 800 arestas¶

In [157]:
for title, G in networks_periodos.items():
    pos = get_geo_positions(G)

    edges_with_weights = [(u, v, d['weight']) for u, v, d in G.edges(data=True)]
    edges_with_weights.sort(key=lambda x: x[2], reverse=True)
    n_top = int(len(edges_with_weights) * 0.10)
    edges_sample = [(u, v) for u, v, w in edges_with_weights[:n_top]]

    # espessura proporcional ao peso
    raw_weights = [w for _, _, w in edges_with_weights[:n_top]]
    w_min, w_max = min(raw_weights), max(raw_weights)
    edge_widths = [min(0.5 + w / 100_000_000, 5) for w in raw_weights]

    nodes = list(pos.keys())
    node_sizes = [min(G.degree(n) * 2, 100) for n in nodes]

    fig = plt.figure(figsize=(15, 10))
    ax = plt.axes(projection=ccrs.PlateCarree())

    ax.add_feature(cfeature.LAND, facecolor='lightgray')
    ax.add_feature(cfeature.OCEAN, facecolor='lightblue')
    ax.add_feature(cfeature.COASTLINE, linewidth=0.5)
    ax.add_feature(cfeature.BORDERS, linewidth=0.5, alpha=0.5)

    nx.draw_networkx_nodes(G, pos, node_size=node_sizes, node_color='b', alpha=0.5, ax=ax)

    nx.draw_networkx_edges(G,pos,edgelist=edges_sample,width=edge_widths,edge_color='darkgreen',
                           alpha=0.4,arrows=True,arrowsize=5,connectionstyle='arc3,rad=0.1',ax=ax)

    ax.set_global()
    plt.title(f"Rede de Exportação de Máscaras Cirúrgicas - {title}", fontsize=16)
    plt.show()
No description has been provided for this image
No description has been provided for this image
No description has been provided for this image

3.2.1. Estatísticas básicas¶

In [159]:
network_stats_per_df = pd.DataFrame([
    {
        'Período': year,
        'Nós': G.number_of_nodes(),
        'Arestas': G.number_of_edges(),
        'Densidade': nx.density(G),
        'Grau Médio': sum(dict(G.degree()).values()) / G.number_of_nodes(),
        'Componentes Conectadas Fracamente': nx.number_weakly_connected_components(G),
        'Tamanho da Maior Componente': len(max(nx.weakly_connected_components(G), key=len))
    }
    for year, G in networks_periodos.items()
])

# mostrar a tabela
print("\nEstatísticas das Redes de Exportação por Ano:")
display(network_stats_per_df)
Estatísticas das Redes de Exportação por Ano:
Período Nós Arestas Densidade Grau Médio Componentes Conectadas Fracamente Tamanho da Maior Componente
0 Pré-Pandemia 159 1136 0.045219 14.289308 1 159
1 Durante Pandemia 175 1323 0.043448 15.120000 1 175
2 Pós-Pandemia 167 1361 0.049095 16.299401 1 167

3.2.2. Medidas de Centralidade¶

In [161]:
# clcular centralidades para cada período e guardar em df
centralidade_periodos = {}

for periodo, G in networks_periodos.items():
    print(f"\n=== Centralidades - {periodo} ===")

    # calcular centralidades
    grau_total = dict(G.degree())
    grau_in = dict(G.in_degree())
    grau_out = dict(G.out_degree())
    betweenness = nx.betweenness_centrality(G, normalized=True)
    closeness = nx.closeness_centrality(G)
    eigenvector = nx.eigenvector_centrality(G)
    pagerank = nx.pagerank(G, alpha=0.85)



    #construir df
    
    df_centralidades = pd.DataFrame({
        'Nó': list(G.nodes()),
        'Grau Total': pd.Series(grau_total),
        'Grau Entrada': pd.Series(grau_in),
        'Grau Saída': pd.Series(grau_out),
        'Betweenness': pd.Series(betweenness),
        'Closeness': pd.Series(closeness),
        'Eigenvector': pd.Series(eigenvector),
        'Page Rank': pd.Series(pagerank),
    })

    df_centralidades = df_centralidades.set_index('Nó')
    df_centralidades = df_centralidades.sort_values('Grau Total', ascending=False)

    #guardar 
    centralidade_periodos[periodo] = df_centralidades

    display(df_centralidades.head(10)) 
=== Centralidades - Pré-Pandemia ===
Grau Total Grau Entrada Grau Saída Betweenness Closeness Eigenvector Page Rank
Nó
CHN 152 19 133 0.114124 0.221711 0.154300 0.012788
DEU 107 37 70 0.076492 0.261302 0.260183 0.073282
HKG 102 10 92 0.023828 0.201833 0.083668 0.004925
USA 88 33 55 0.098182 0.258990 0.215394 0.044367
IND 78 9 69 0.035096 0.199087 0.070155 0.003762
TUR 77 14 63 0.022569 0.207559 0.131956 0.005606
FRA 69 31 38 0.034094 0.243882 0.220822 0.035796
NLD 67 24 43 0.019269 0.230440 0.188709 0.023910
POL 56 23 33 0.012622 0.232268 0.197858 0.026402
ITA 56 25 31 0.019892 0.237934 0.190068 0.023499
=== Centralidades - Durante Pandemia ===
Grau Total Grau Entrada Grau Saída Betweenness Closeness Eigenvector Page Rank
Nó
CHN 197 34 163 0.198836 0.284665 0.188756 0.017235
DEU 114 53 61 0.064766 0.335213 0.277015 0.065098
USA 109 45 64 0.084185 0.306563 0.220061 0.094732
TUR 87 15 72 0.021002 0.235972 0.112164 0.005353
FRA 87 41 46 0.031644 0.296428 0.233230 0.052897
NLD 82 35 47 0.017209 0.273800 0.206814 0.021847
HKG 76 24 52 0.017393 0.256199 0.128535 0.009771
GBR 68 33 35 0.010632 0.269683 0.197584 0.015855
IND 66 11 55 0.009343 0.228457 0.065748 0.004746
ITA 64 30 34 0.014006 0.265688 0.202409 0.027568
=== Centralidades - Pós-Pandemia ===
Grau Total Grau Entrada Grau Saída Betweenness Closeness Eigenvector Page Rank
Nó
CHN 176 29 147 0.139261 0.258890 0.170584 0.010085
DEU 111 47 64 0.064736 0.305737 0.251804 0.064468
USA 107 43 64 0.106485 0.305737 0.206620 0.091629
TUR 95 19 76 0.036093 0.241372 0.129955 0.005128
FRA 82 38 44 0.028375 0.284092 0.215068 0.045997
NLD 77 30 47 0.010932 0.260995 0.180750 0.019284
IND 72 12 60 0.016060 0.222933 0.071619 0.002990
GBR 72 33 39 0.016308 0.267520 0.199229 0.015623
POL 72 34 38 0.011436 0.269768 0.204632 0.028959
ITA 67 32 35 0.011521 0.265309 0.205465 0.025458
In [162]:
for periodo, G in networks_periodos.items():
    print(f"\n=== Centralidades - {periodo} ===")
    
    # maior componente fortemente conectada
    largest_scc = max(nx.strongly_connected_components(G), key=len)
    G_scc = G.subgraph(largest_scc).copy()
    
    # excentricidade para a maior componente fortemente conectada
    excentricidade = nx.eccentricity(G_scc)
    
    #df com os resultados
    df_excentricidade = pd.DataFrame({
        'Excentricidade': pd.Series(excentricidade),
    })
    df_excentricidade = df_excentricidade.sort_values('Excentricidade', ascending=True)
    display(df_excentricidade.head(10))
    raio = min(excentricidade.values())
    print(f"Raio do grafo para {periodo}: {raio}")
=== Centralidades - Pré-Pandemia ===
Excentricidade
GBR 2
CHN 2
DEU 2
NLD 2
POL 2
SWE 2
ITA 2
USA 2
VNM 2
FRA 2
Raio do grafo para Pré-Pandemia: 2

=== Centralidades - Durante Pandemia ===
Excentricidade
CHN 1
UKR 2
IND 2
SGP 2
GBR 2
USA 2
ITA 2
IDN 2
SVK 2
NLD 2
Raio do grafo para Durante Pandemia: 1

=== Centralidades - Pós-Pandemia ===
Excentricidade
GBR 2
TUR 2
ESP 2
CHE 2
IND 2
CHN 2
USA 2
CZE 2
NLD 2
DEU 2
Raio do grafo para Pós-Pandemia: 2

3.2.3 - Propriedades de Conectividade¶

In [164]:
for periodo, G in networks_periodos.items():
    print(f'\n\n--- Período {periodo} ---')

    print('É fortemente conexo?', nx.is_strongly_connected(G))
    print('É fracamente conexo?', nx.is_weakly_connected(G))
    
    print('Número de componentes fortemente conexas:', nx.number_strongly_connected_components(G))
    print('Número de componentes fracamente conexas:', nx.number_weakly_connected_components(G))

    print('\nTop 3 componentes fortemente conexas:')
    for c in sorted(nx.strongly_connected_components(G), key=len, reverse=True)[:3]:
        print(c)

    print('\nTop 3 componentes fracamente conexas:')
    for c in sorted(nx.weakly_connected_components(G), key=len, reverse=True)[:3]:
        print(c)

--- Período Pré-Pandemia ---
É fortemente conexo? False
É fracamente conexo? True
Número de componentes fortemente conexas: 91
Número de componentes fracamente conexas: 1

Top 3 componentes fortemente conexas:
{'GBR', 'ISR', 'MYS', 'MEX', 'PRY', 'BEL', 'LTU', 'DOM', 'HUN', 'SLV', 'PHL', 'ARE', 'KOR', 'MAR', 'ROU', 'SVK', 'PAK', 'NLD', 'DEU', 'IRL', 'UKR', 'IDN', 'MMR', 'EST', 'SVN', 'COL', 'CHN', 'MDG', 'IND', 'SGP', 'CHL', 'CHE', 'SAU', 'TUR', 'PRT', 'MDA', 'BLR', 'LUX', 'LAO', 'KAZ', 'BRA', 'ESP', 'BHR', 'HRV', 'NOR', 'BGR', 'LVA', 'MKD', 'THA', 'USA', 'CZE', 'DNK', 'POL', 'SWE', 'SRB', 'AUS', 'ITA', 'KWT', 'ZAF', 'JPN', 'KHM', 'ECU', 'VNM', 'RUS', 'HKG', 'FRA', 'PER', 'FIN', 'BIH'}
{'JOR'}
{'QAT'}

Top 3 componentes fracamente conexas:
{'UGA', 'MEX', 'MYS', 'URY', 'BEL', 'SLV', 'PHL', 'ARE', 'AZE', 'DEU', 'IRL', 'COD', 'MMR', 'AUT', 'GMB', 'TGO', 'MDG', 'SDN', 'SAU', 'MUS', 'TUR', 'KAZ', 'LUX', 'BWA', 'BRA', 'NOR', 'MRT', 'SRB', 'SYR', 'JPN', 'KHM', 'BHS', 'RUS', 'DJI', 'UZB', 'HKG', 'TTO', 'GUM', 'AFG', 'RWA', 'CHN', 'IND', 'SGP', 'MDV', 'PRT', 'BHR', 'SEN', 'SOM', 'ESP', 'GTM', 'SWZ', 'IRN', 'USA', 'COM', 'DNK', 'CRI', 'EGY', 'ITA', 'NAM', 'YEM', 'GHA', 'CYP', 'LBN', 'MAC', 'GUY', 'ALB', 'ETH', 'GAB', 'KEN', 'NZL', 'TZA', 'ZMB', 'DOM', 'PRY', 'MNG', 'CUB', 'ROU', 'SVK', 'NLD', 'UKR', 'DZA', 'EST', 'COL', 'FJI', 'CHE', 'CYM', 'BOL', 'PYF', 'BRN', 'HRV', 'THA', 'JAM', 'POL', 'ARG', 'JOR', 'MLT', 'CAN', 'KGZ', 'PAN', 'AND', 'FIN', 'GBR', 'NIC', 'ISR', 'COG', 'VEN', 'TKM', 'BRB', 'NGA', 'LTU', 'HUN', 'TJK', 'KOR', 'MAR', 'IRQ', 'PAK', 'GRC', 'LSO', 'ISL', 'IDN', 'SVN', 'NPL', 'PRK', 'CHL', 'HND', 'BGD', 'LBY', 'PNG', 'TUN', 'LAO', 'MDA', 'BLR', 'CIV', 'QAT', 'BEN', 'MOZ', 'GEO', 'MWI', 'BGR', 'LVA', 'MKD', 'CZE', 'SWE', 'KWT', 'CMR', 'HTI', 'AUS', 'ZAF', 'ECU', 'VNM', 'NCL', 'FRA', 'OMN', 'LKA', 'PER', 'AGO', 'ZWE', 'GIN', 'BIH'}


--- Período Durante Pandemia ---
É fortemente conexo? False
É fracamente conexo? True
Número de componentes fortemente conexas: 96
Número de componentes fracamente conexas: 1

Top 3 componentes fortemente conexas:
{'URY', 'MEX', 'MYS', 'BEL', 'SLV', 'PHL', 'DEU', 'IRL', 'MMR', 'MDG', 'TUR', 'LUX', 'BRA', 'NOR', 'SRB', 'JPN', 'KHM', 'RUS', 'UZB', 'HKG', 'CHN', 'IND', 'SGP', 'PRT', 'BHR', 'ESP', 'GTM', 'USA', 'DNK', 'EGY', 'ITA', 'GHA', 'CYP', 'MAC', 'PRY', 'DOM', 'ROU', 'SVK', 'NLD', 'UKR', 'EST', 'COL', 'CHE', 'BOL', 'HRV', 'THA', 'POL', 'ARG', 'CAN', 'FIN', 'GBR', 'NIC', 'ISR', 'LTU', 'HUN', 'KOR', 'MAR', 'PAK', 'GRC', 'IDN', 'SVN', 'CHL', 'TUN', 'LAO', 'MDA', 'BLR', 'GEO', 'ARM', 'BGR', 'MKD', 'LVA', 'CZE', 'SWE', 'AUS', 'ZAF', 'ECU', 'VNM', 'FRA', 'PER', 'BIH'}
{'AFG'}
{'AGO'}

Top 3 componentes fracamente conexas:
{'UGA', 'URY', 'MYS', 'MEX', 'BEL', 'SLV', 'PHL', 'ARE', 'AZE', 'DEU', 'IRL', 'COD', 'MMR', 'AUT', 'GMB', 'TGO', 'MDG', 'SDN', 'MUS', 'SAU', 'TUR', 'KAZ', 'LUX', 'BWA', 'BRA', 'MRT', 'NOR', 'LBR', 'TLS', 'SRB', 'SYR', 'JPN', 'KHM', 'BHS', 'CPV', 'RUS', 'DJI', 'UZB', 'HKG', 'TTO', 'AFG', 'MNE', 'RWA', 'CHN', 'SGP', 'IND', 'TCD', 'MDV', 'PRT', 'BHR', 'SEN', 'SUR', 'GRD', 'SOM', 'ESP', 'GTM', 'SWZ', 'IRN', 'USA', 'COM', 'DNK', 'CRI', 'EGY', 'ITA', 'NAM', 'YEM', 'BLZ', 'GHA', 'BFA', 'CYP', 'GUY', 'LBN', 'MAC', 'ALB', 'SLE', 'ETH', 'GAB', 'KEN', 'NZL', 'PRY', 'DOM', 'TZA', 'ZMB', 'MNG', 'CUB', 'ROU', 'SVK', 'NLD', 'UKR', 'DZA', 'EST', 'COL', 'NER', 'FJI', 'CHE', 'CYM', 'BOL', 'PYF', 'BRN', 'HRV', 'THA', 'JAM', 'POL', 'ARG', 'JOR', 'MLT', 'CAN', 'DMA', 'KGZ', 'PAN', 'AND', 'FIN', 'GBR', 'NIC', 'COG', 'ISR', 'TKM', 'VEN', 'BRB', 'NGA', 'LTU', 'HUN', 'TJK', 'KOR', 'MAR', 'PSE', 'PAK', 'GRC', 'IRQ', 'LSO', 'ISL', 'IDN', 'SVN', 'NPL', 'PRK', 'CHL', 'HND', 'BGD', 'LBY', 'TUN', 'PNG', 'LAO', 'MDA', 'BLR', 'CIV', 'BEN', 'MWI', 'MOZ', 'GEO', 'QAT', 'ARM', 'SSD', 'BGR', 'LVA', 'MKD', 'VCT', 'CZE', 'SWE', 'CMR', 'HTI', 'KWT', 'AUS', 'MLI', 'ZAF', 'ECU', 'VNM', 'NCL', 'FRA', 'PER', 'LKA', 'OMN', 'AGO', 'ZWE', 'GIN', 'BIH'}


--- Período Pós-Pandemia ---
É fortemente conexo? False
É fracamente conexo? True
Número de componentes fortemente conexas: 94
Número de componentes fracamente conexas: 1

Top 3 componentes fortemente conexas:
{'GBR', 'ISR', 'NZL', 'MEX', 'URY', 'PRY', 'BEL', 'LTU', 'DOM', 'HUN', 'SLV', 'MYS', 'PHL', 'KOR', 'MAR', 'ROU', 'SVK', 'PAK', 'GRC', 'DEU', 'IRL', 'NLD', 'UKR', 'IDN', 'AUT', 'MMR', 'EST', 'SVN', 'COL', 'CHN', 'MDG', 'IND', 'SGP', 'CHL', 'CHE', 'SAU', 'TUN', 'BOL', 'TUR', 'LAO', 'PRT', 'KAZ', 'MDA', 'BHR', 'BRA', 'ESP', 'GEO', 'GTM', 'HRV', 'NOR', 'BGR', 'LVA', 'MKD', 'THA', 'USA', 'CZE', 'DNK', 'POL', 'ARG', 'SWE', 'AUS', 'ITA', 'SRB', 'ZAF', 'JPN', 'KHM', 'VNM', 'CAN', 'UZB', 'HKG', 'FRA', 'GHA', 'FIN', 'BIH'}
{'ECU'}
{'ARE'}

Top 3 componentes fracamente conexas:
{'UGA', 'URY', 'MEX', 'MYS', 'BEL', 'SLV', 'PHL', 'ARE', 'AZE', 'DEU', 'IRL', 'COD', 'MMR', 'AUT', 'TGO', 'MDG', 'SDN', 'MUS', 'SAU', 'TUR', 'KAZ', 'LUX', 'BWA', 'BRA', 'MRT', 'NOR', 'LBR', 'SRB', 'SYR', 'JPN', 'KHM', 'BHS', 'RUS', 'DJI', 'UZB', 'HKG', 'TTO', 'AFG', 'MNE', 'GRL', 'FRO', 'RWA', 'CHN', 'IND', 'SGP', 'MDV', 'PRT', 'BHR', 'SEN', 'SUR', 'SOM', 'ESP', 'GTM', 'SWZ', 'IRN', 'USA', 'DNK', 'CRI', 'EGY', 'ITA', 'NAM', 'BLZ', 'YEM', 'GHA', 'BFA', 'CYP', 'GUY', 'LBN', 'MAC', 'ALB', 'ETH', 'GAB', 'KEN', 'NZL', 'PRY', 'DOM', 'TZA', 'ZMB', 'MNG', 'CUB', 'ROU', 'SVK', 'NLD', 'UKR', 'DZA', 'EST', 'COL', 'FJI', 'CHE', 'CYM', 'BOL', 'PYF', 'BRN', 'HRV', 'THA', 'JAM', 'POL', 'ARG', 'JOR', 'MLT', 'CAN', 'KGZ', 'PAN', 'AND', 'GNQ', 'FIN', 'GBR', 'NIC', 'ISR', 'COG', 'TKM', 'VEN', 'BRB', 'NGA', 'LTU', 'HUN', 'TJK', 'KOR', 'MAR', 'PAK', 'GRC', 'IRQ', 'LSO', 'ISL', 'IDN', 'SVN', 'NPL', 'PRK', 'CHL', 'HND', 'BGD', 'LBY', 'PNG', 'TUN', 'LAO', 'MDA', 'BLR', 'CIV', 'BEN', 'MWI', 'MOZ', 'GEO', 'QAT', 'ARM', 'BGR', 'LVA', 'MKD', 'CZE', 'SWE', 'CMR', 'HTI', 'KWT', 'AUS', 'MLI', 'CUW', 'ZAF', 'ECU', 'VNM', 'NCL', 'FRA', 'LKA', 'OMN', 'PER', 'AGO', 'ZWE', 'GIN', 'BIH'}

3.2.4 - Medidas de Distância¶

In [171]:
G_pre = create_network_period(periodos["Pré-Pandemia (2015-2018)"], "Pré", threshold=0)
G_durante = create_network_period(periodos["Durante a Pandemia (2020-2021)"], "Durante", threshold=0)
G_pos = create_network_period(periodos["Pós-Pandemia (2022-2024)"], "Pós", threshold=0)

# dicionários de graus
grau_entrada_pre = dict(G_pre.in_degree())
grau_saida_pre = dict(G_pre.out_degree())
grau_entrada_durante = dict(G_durante.in_degree())
grau_saida_durante = dict(G_durante.out_degree())

# top países por grau de entrada
top_entrada_pre = pd.Series(grau_entrada_pre).sort_values(ascending=False).head(10)
top_entrada_durante = pd.Series(grau_entrada_durante).sort_values(ascending=False).head(10)

# top países por grau de entrada
top_saida_pre = pd.Series(grau_saida_pre).sort_values(ascending=False).head(10)
top_saida_durante = pd.Series(grau_saida_durante).sort_values(ascending=False).head(10)

# plot
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle("Top 10 Países por Grau de Entrada e Saída", fontsize=16)

top_entrada_pre.plot(kind="bar", ax=axes[0, 0], title="Entrada - Pré-pandemia", color='steelblue')
top_entrada_durante.plot(kind="bar", ax=axes[0, 1], title="Entrada - Durante pandemia", color='darkorange')
top_saida_pre.plot(kind="bar", ax=axes[1, 0], title="Saída - Pré-pandemia", color='steelblue')
top_saida_durante.plot(kind="bar", ax=axes[1, 1], title="Saída - Durante pandemia", color='darkorange')

for ax in axes.flatten():
    ax.set_ylabel("Número de conexões")

plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()
No description has been provided for this image
In [173]:
def calcular_medidas_distancia(G, periodo, top_n=10):
    print(f"\n=== Medidas de Distância - {periodo} ===")
    
    # identificar a maior componente fortemente conectada (SCC)
    scc = list(nx.strongly_connected_components(G))
    scc.sort(key=len, reverse=True)  # ordenar por tamanho
    maior_scc = scc[0]
    G_scc = G.subgraph(maior_scc).copy()  # fazer o subgrafo da primeira - maior
    
    print(f"Maior componente fortemente conectada tem {len(G_scc)} nós.")
    print()

    #grande parte do código destas medidas -  abaixo e algum acima - é das aulas, dos notebooks do professor, adaptei apenas
    # Excentricidade: Top 5 menor e maior
    excent = nx.eccentricity(G_scc)
    excent_df = pd.DataFrame(list(excent.items()), columns=['Nó', 'Excentricidade'])
    excent_df = excent_df.sort_values('Excentricidade')
    
    print("Top 5 nós com menor excentricidade (mais centrais):")
    display(excent_df.head(5))
    print("Top 5 nós com maior excentricidade (mais periféricos):")
    display(excent_df.tail(5))
    print()
    
    # Raio
    r = nx.radius(G_scc)
    print('Raio de G:', r)
    print()
    
    # Diâmetro
    d = nx.diameter(G_scc)
    print('Diâmetro de G:', d)
    print()
    
    # Centro
    centro = nx.center(G_scc)
    print('Centro de G (nodos com excentricidade = raio):', centro)
    print()
    
    # Periferia
    periferia = nx.periphery(G_scc)
    print('Periferia de G (nodos com excentricidade = diâmetro):', periferia)
    print()
    
    # Graus de entrada e saída: Top 10
    in_deg = dict(G.in_degree())
    out_deg = dict(G.out_degree())
    
    in_deg_df = pd.DataFrame(list(in_deg.items()), columns=['Nó', 'Grau de Entrada'])
    out_deg_df = pd.DataFrame(list(out_deg.items()), columns=['Nó', 'Grau de Saída'])
    
    in_deg_df = in_deg_df.sort_values('Grau de Entrada', ascending=False)
    out_deg_df = out_deg_df.sort_values('Grau de Saída', ascending=False)
    
    print(f"Top {top_n} nós com maior grau de entrada:")
    display(in_deg_df.head(top_n))
    print(f"Top {top_n} nós com maior grau de saída:")
    display(out_deg_df.head(top_n))
    print()
    
    # Grau médio
    gm_in = sum(in_deg.values()) / len(G.nodes)
    gm_out = sum(out_deg.values()) / len(G.nodes)
    print('Grau médio de entrada de G:', gm_in)
    print('Grau médio de saída de G:', gm_out)
    print()
    
    # Average path length
    apl = nx.average_shortest_path_length(G_scc)
    print('Average path length de G:', apl)
    print()

# aplicar a cada período
for periodo, G in networks_periodos.items():
    calcular_medidas_distancia(G, periodo, top_n=10)
=== Medidas de Distância - Pré-Pandemia ===
Maior componente fortemente conectada tem 69 nós.

Top 5 nós com menor excentricidade (mais centrais):
Nó Excentricidade
0 GBR 2
26 CHN 2
18 DEU 2
17 NLD 2
52 POL 2
Top 5 nós com maior excentricidade (mais periféricos):
Nó Excentricidade
57 KWT 5
61 ECU 5
66 PER 5
32 SAU 6
42 BHR 7
Raio de G: 2

Diâmetro de G: 7

Centro de G (nodos com excentricidade = raio): ['GBR', 'NLD', 'DEU', 'CHN', 'USA', 'POL', 'SWE', 'ITA', 'VNM', 'HKG', 'FRA']

Periferia de G (nodos com excentricidade = diâmetro): ['BHR']

Top 10 nós com maior grau de entrada:
Nó Grau de Entrada
17 DEU 37
21 USA 33
29 FRA 31
30 GBR 26
32 ITA 25
44 RUS 25
34 NLD 24
40 SWE 23
35 POL 23
26 DNK 22
Top 10 nós com maior grau de saída:
Nó Grau de Saída
16 CHN 133
84 HKG 92
17 DEU 70
3 IND 69
130 TUR 63
21 USA 55
34 NLD 43
29 FRA 38
135 VNM 33
35 POL 33
Grau médio de entrada de G: 7.144654088050315
Grau médio de saída de G: 7.144654088050315

Average path length de G: 2.2440323955669226


=== Medidas de Distância - Durante Pandemia ===
Maior componente fortemente conectada tem 80 nós.

Top 5 nós com menor excentricidade (mais centrais):
Nó Excentricidade
20 CHN 1
39 UKR 2
21 IND 2
22 SGP 2
50 GBR 2
Top 5 nós com maior excentricidade (mais periféricos):
Nó Excentricidade
44 HRV 3
28 DNK 3
79 BIH 3
32 CYP 4
47 ARG 4
Raio de G: 1

Diâmetro de G: 4

Centro de G (nodos com excentricidade = raio): ['CHN']

Periferia de G (nodos com excentricidade = diâmetro): ['CYP', 'ARG']

Top 10 nós com maior grau de entrada:
Nó Grau de Entrada
17 DEU 53
12 USA 45
22 FRA 41
29 NLD 35
5 CHN 34
23 GBR 33
26 ITA 30
13 BEL 28
20 ESP 27
30 POL 25
Top 10 nós com maior grau de saída:
Nó Grau de Saída
5 CHN 163
43 TUR 72
12 USA 64
17 DEU 61
8 IND 55
7 HKG 52
29 NLD 47
22 FRA 46
160 VNM 40
23 GBR 35
Grau médio de entrada de G: 7.56
Grau médio de saída de G: 7.56

Average path length de G: 2.0765822784810126


=== Medidas de Distância - Pós-Pandemia ===
Maior componente fortemente conectada tem 74 nós.

Top 5 nós com menor excentricidade (mais centrais):
Nó Excentricidade
0 GBR 2
38 TUR 2
45 ESP 2
34 CHE 2
31 IND 2
Top 5 nós com maior excentricidade (mais periféricos):
Nó Excentricidade
4 URY 4
68 UZB 4
35 SAU 4
58 ARG 4
37 BOL 5
Raio de G: 2

Diâmetro de G: 5

Centro de G (nodos com excentricidade = raio): ['GBR', 'HUN', 'MYS', 'KOR', 'SVK', 'PAK', 'DEU', 'NLD', 'CHN', 'IND', 'CHE', 'TUR', 'ESP', 'USA', 'CZE', 'DNK', 'POL', 'SWE', 'ITA', 'KHM', 'VNM', 'CAN', 'HKG', 'FRA']

Periferia de G (nodos com excentricidade = diâmetro): ['BOL']

Top 10 nós com maior grau de entrada:
Nó Grau de Entrada
20 DEU 47
15 USA 43
21 FRA 38
26 POL 34
8 GBR 33
25 ITA 32
38 NLD 30
30 BEL 29
7 CHN 29
40 SWE 26
Top 10 nós com maior grau de saída:
Nó Grau de Saída
7 CHN 147
55 TUR 76
20 DEU 64
15 USA 64
88 IND 60
38 NLD 47
21 FRA 44
8 GBR 39
26 POL 38
31 DNK 36
Grau médio de entrada de G: 8.149700598802395
Grau médio de saída de G: 8.149700598802395

Average path length de G: 2.0510921880784894

3.2.5 - Comunidades¶

In [175]:
# dicionários para guardar resultados por período
communities_by_period = {}
colors_by_period = {}
modularity_by_period = {}
partition_quality_by_period = {}

for period, G in networks_periodos.items():
    print(f"\nPeriodo: {period}")
    
    # aplicar Louvain diretamente no grafo direcionado com pesos
    com_louvain = list(nx.community.louvain_communities(G, weight='weight', seed=42))
    communities_by_period[period] = com_louvain

    print(f"Número de comunidades: {len(com_louvain)}")
    for i, com in enumerate(com_louvain):
        print(f" - Comunidade {i+1}: {len(com)} nós")

    # cor aos nós
    node_color = [(0.5, 0.5, 0.5) for _ in G.nodes()]
    for i, v in enumerate(G.nodes()):
        for j in range(len(com_louvain)):
            if v in com_louvain[j]:
                node_color[i] = (0.25*j/10, 2.1*j/10, 1-2*j/10) #-> ideia encontrada no stackoverflow
    colors_by_period[period] = node_color

    #modularidade e qualidade
    m1 = nx.community.modularity(G, communities=com_louvain, weight='weight', resolution=1)
    q1 = nx.community.partition_quality(G, partition=com_louvain)

    modularity_by_period[period] = m1
    partition_quality_by_period[period] = q1

    print(f"Índice de modularidade: {m1:.3f}")
    print(f"Cobertura: {q1[0]:.3f} | Performance: {q1[1]:.3f}")

    # desenhar grafo com comunidades
    pos = get_geo_positions(G)
    plt.figure(figsize=(20,15))
    nx.draw_networkx(G, pos, node_color=node_color, with_labels=True, node_size=600,
                     node_shape='o', alpha=0.8, edge_color='gray', width=0.5)
    plt.title(f"Comunidades Louvain - {period}")
    plt.show()

    # bgrafos por comunidade
    f, axs = plt.subplots(nrows=math.ceil(len(com_louvain)/2), ncols=2, figsize=(22, 20))
    axs = axs.flatten()
    for i in range(len(com_louvain)):
        G1 = G.subgraph(com_louvain[i])
        node_color1 = [node_color[j] for j, v in enumerate(G.nodes()) if v in com_louvain[i]]
        nx.draw_networkx_nodes(G1, pos, nodelist=com_louvain[i], node_color=node_color1,node_size=600, node_shape='o', alpha=0.8, ax=axs[i])
        nx.draw_networkx(G1, pos, with_labels=True, node_size=0, edge_color='gray',width=0.5, ax=axs[i])
        axs[i].set_title(f"Comunidade {i+1} - {period}")
    plt.tight_layout()
    plt.show()
Periodo: Pré-Pandemia
Número de comunidades: 4
 - Comunidade 1: 57 nós
 - Comunidade 2: 62 nós
 - Comunidade 3: 38 nós
 - Comunidade 4: 2 nós
Índice de modularidade: 0.232
Cobertura: 0.644 | Performance: 0.679
No description has been provided for this image
No description has been provided for this image
Periodo: Durante Pandemia
Número de comunidades: 4
 - Comunidade 1: 12 nós
 - Comunidade 2: 28 nós
 - Comunidade 3: 79 nós
 - Comunidade 4: 56 nós
Índice de modularidade: 0.193
Cobertura: 0.675 | Performance: 0.683
No description has been provided for this image
No description has been provided for this image
Periodo: Pós-Pandemia
Número de comunidades: 4
 - Comunidade 1: 19 nós
 - Comunidade 2: 89 nós
 - Comunidade 3: 56 nós
 - Comunidade 4: 3 nós
Índice de modularidade: 0.306
Cobertura: 0.686 | Performance: 0.612
No description has been provided for this image
No description has been provided for this image
In [179]:
networks = {'Pré': G_pre, 'Durante': G_durante, 'Pós': G_pos}
summary_data = []

for year, G in networks.items():
    stats = {
        'Período': year,
        'Nº Nós': G.number_of_nodes(),
        'Nº Arestas': G.number_of_edges(),
        'Densidade': nx.density(G),
        'Grau Médio': sum(dict(G.degree()).values()) / G.number_of_nodes(),
        'Grau Máximo': max(dict(G.degree()).values()),
        'Componentes Fortes': nx.number_strongly_connected_components(G) if G.is_directed() else '-',
        'Clustering Médio': nx.average_clustering(G.to_undirected())
    }
    summary_data.append(stats)

df_summary = pd.DataFrame(summary_data)
display(df_summary)
Período Nº Nós Nº Arestas Densidade Grau Médio Grau Máximo Componentes Fortes Clustering Médio
0 Pré 230 7584 0.143991 65.947826 291 101 0.795445
1 Durante 228 8057 0.155673 70.675439 311 102 0.803318
2 Pós 230 8106 0.153902 70.486957 290 109 0.817830
In [183]:
clustering_pre = nx.transitivity(G_pre.to_undirected())
clustering_dur = nx.transitivity(G_durante.to_undirected())
clustering_pos = nx.transitivity(G_pos.to_undirected())

print(f"Clustering Pre (Transitivity): {clustering_pre:.4f}")
print()
print(f"Clustering Dur (Transitivity): {clustering_dur:.4f}")
print()
print(f"Clustering Pos (Transitivity): {clustering_pos:.4f}")
Clustering Pre (Transitivity): 0.4942

Clustering Dur (Transitivity): 0.5065

Clustering Pos (Transitivity): 0.5068
In [185]:
clustering_pre = nx.transitivity(G_pre)
clustering_dur = nx.transitivity(G_durante)
clustering_pos = nx.transitivity(G_pos)

print(f"Clustering Pre (Transitivity): {clustering_pre:.4f}")
print()
print(f"Clustering Dur (Transitivity): {clustering_dur:.4f}")
print()
print(f"Clustering Pos (Transitivity): {clustering_pos:.4f}")
Clustering Pre (Transitivity): 0.3038

Clustering Dur (Transitivity): 0.3170

Clustering Pos (Transitivity): 0.3085
In [187]:
cluster_china_pre = nx.clustering(G_pre,'CHN')
cluster_china_dur = nx.clustering(G_durante,'CHN')
cluster_china_pos = nx.clustering(G_pos,'CHN')
print(f'China Pré: {cluster_china_pre} \nChina Durante: {cluster_china_dur} \nChina Pós: {cluster_china_pos}')
print()
cluster_usa_pre = nx.clustering(G_pre,'USA')
cluster_usa_dur = nx.clustering(G_durante,'USA')
cluster_usa_pos = nx.clustering(G_pos,'USA')
print(f'USA Pré: {cluster_usa_pre} \nUSA Durante: {cluster_usa_dur} \nUSA Pós: {cluster_usa_pos}')
print()
cluster_deu_pre = nx.clustering(G_pre,'DEU')
cluster_deu_dur = nx.clustering(G_durante,'DEU')
cluster_deu_pos = nx.clustering(G_pos,'DEU')
print(f'DEU Pré: {cluster_deu_pre} \nDEU Durante: {cluster_deu_dur} \nDEU Pós: {cluster_deu_pos}')
China Pré: 0.2528732902735562 
China Durante: 0.26157460343859795 
China Pós: 0.279345703125

USA Pré: 0.2925983352374735 
USA Durante: 0.2930787981062204 
USA Pós: 0.29561004784688993

DEU Pré: 0.2777722735491541 
DEU Durante: 0.29843929365215055 
DEU Pós: 0.3046244529332895

teste -> faz sentido definir bridges desta forma?¶

In [191]:
def analisar_bridges(G, periodo, thresh_cluster=0.3, thresh_betw=0.02):
    clustering_local = nx.clustering(G)  
    betweenness = nx.betweenness_centrality(G)

    df2 = pd.DataFrame({
        'Nó': list(clustering_local.keys()),
        'Clustering': list(clustering_local.values()),
        'Betweenness': list(betweenness.values())
    })

    df2['Período'] = periodo
    df2['Bridge?'] = (df2['Clustering'] < thresh_cluster) & (df2['Betweenness'] > thresh_betw)

    return df2.sort_values(by='Betweenness', ascending=False)

df_bridges_pre = analisar_bridges(G_pre, 'Pré-Pandemia')
df_bridges_dur = analisar_bridges(G_durante, 'Durante a Pandemia')
df_bridges_pos = analisar_bridges(G_pos, 'Pós-Pandemia')

df_bridges_todos = pd.concat([df_bridges_pre, df_bridges_dur, df_bridges_pos])
df_bridges_todos[df_bridges_todos['Bridge?'] == True]
Out[191]:
Nó Clustering Betweenness Período Bridge?
58 CHN 0.252873 0.035045 Pré-Pandemia True
39 USA 0.292598 0.034894 Pré-Pandemia True
64 DEU 0.277772 0.034677 Pré-Pandemia True
15 FRA 0.287857 0.026704 Pré-Pandemia True
26 NLD 0.288453 0.021279 Pré-Pandemia True
5 CHN 0.261575 0.045976 Durante a Pandemia True
29 USA 0.293079 0.044686 Durante a Pandemia True
11 DEU 0.298439 0.025234 Durante a Pandemia True
31 USA 0.295610 0.045835 Pós-Pandemia True
18 GBR 0.298247 0.026175 Pós-Pandemia True
9 CHN 0.279346 0.021940 Pós-Pandemia True

3.3 - Mapas Temporais¶

In [215]:
def criar_mapa_temp(networks, coords):
    fig = go.Figure()
    periods = list(networks.keys())
    
    for period_idx, (period_name, G) in enumerate(networks.items()):

        nodes_data = [(n, coords[n]['Longitude'], coords[n]['Latitude'], G.degree(n)) 
                      for n in G.nodes() if n in coords]
        node_sizes = [min(degree * 2, 100) for _, _, _, degree in nodes_data]
        
        # filtrar top 10% das arestas por peso 
        edges_with_weights = [(u, v, d['weight']) for u, v, d in G.edges(data=True)
                              if u in coords and v in coords]
        edges_with_weights.sort(key=lambda x: x[2], reverse=True)
        n_top = int(len(edges_with_weights) * 0.10)
        edges_top = edges_with_weights[:n_top]

        # espessura proporcional ao peso
        if edges_top:
            raw_weights = [w for _, _, w in edges_top]
            edge_widths = [min(0.5 + w / 100_000_000, 5) for w in raw_weights]
            max_width = max(edge_widths) if edge_widths else 5
            
            # categorizar por espessura 
            edge_groups = []
            remaining_edges = list(zip(edges_top, edge_widths))
            
            for threshold, opacity in [(0.7, 0.7), (0.3, 0.5), (0, 0.3)]:
                group = [(u, v, w, ew) for (u, v, w), ew in remaining_edges 
                         if ew > threshold * max_width]
                remaining_edges = [((u, v, w), ew) for (u, v, w), ew in remaining_edges 
                                   if ew <= threshold * max_width]
                edge_groups.append((group, opacity))
        else:
            edge_groups = [([], 0.7), ([], 0.5), ([], 0.3)]
        
        # adicionar 3 traços de arestas
        for edges_list, opacity in edge_groups:
            if edges_list:
                avg_width = sum(e[3] for e in edges_list) / len(edges_list)
                lons = [coord for e in edges_list for coord in 
                       [coords[e[0]]['Longitude'], coords[e[1]]['Longitude'], None]]
                lats = [coord for e in edges_list for coord in 
                       [coords[e[0]]['Latitude'], coords[e[1]]['Latitude'], None]]
            else:
                avg_width = 1
                lons, lats = [], []
            
            fig.add_trace(go.Scattergeo(
                lon=lons, lat=lats, mode='lines',
                line=dict(width=avg_width, color=f'rgba(0, 150, 0, {opacity})'),
                hoverinfo='none', showlegend=False,
                visible=(period_idx == 0)
            ))
        
        # adicionar nós
        fig.add_trace(go.Scattergeo(
            lon=[n[1] for n in nodes_data],
            lat=[n[2] for n in nodes_data],
            mode='markers',
            marker=dict(
                size=[s/5 for s in node_sizes],
                color='red', 
                opacity=0.8
            ),
            text=[f"{n[0]}<br>Conexões: {n[3]}" for n in nodes_data],
            hovertemplate='%{text}<extra></extra>',
            name=period_name,
            visible=(period_idx == 0)
        ))
    
    #configo do mapa e slider
    fig.update_geos(
        projection_type='equirectangular',
        showland=True, landcolor='lightgray',
        showocean=True, oceancolor='lightblue',
        showcountries=True, countrycolor='white'
    )
    
    fig.update_layout(
        title="Evolução das Redes de Exportação de Máscaras Cirúrgicas",
        height=600,
        sliders=[dict(
            active=0,
            currentvalue={"prefix": "Período: "},
            steps=[dict(
                label=period,
                method="update",
                args=[{"visible": [j // 4 == i for j in range(len(fig.data))]}]
            ) for i, period in enumerate(periods)]
        )]
    )
    
    return fig


coords_df = pd.read_csv('in-nodes-All.csv')  
coords = coords_df.set_index('Label')[['Longitude', 'Latitude']].to_dict('index')

fig = criar_mapa_temp(networks_periodos, coords)
fig.show()
fig.write_html("evolucao_redes_mascaras_periodos.html")
In [195]:
df.columns
Out[195]:
Index(['refYear', 'refMonth', 'flowDesc', 'reporterISO', 'partnerISO', 'qty',
       'primaryValue', 'unitPrice', 'Exportador', 'Importador'],
      dtype='object')
In [213]:
def create_network(df, year, threshold=0):

    G = nx.DiGraph(name=f"Rede de Exportação de Máscaras Cirúrgicas - {year}")
    
    for _, row in df.iterrows():
        if row['qty'] > threshold:
            G.add_edge(
                row['reporterISO'],
                row['partnerISO'],
                weight=row['qty']
            )
    
    return G

networks_anos = {}
for ano in range(2015, 2025):  
    # filtrar os dados para um ano específico
    dados_ano = df_exports[df_exports['refYear'] == ano]
    networks_anos[str(ano)] = create_network(dados_ano, str(ano), threshold=0)

fig = criar_mapa_temp(networks_anos, coords)
fig.show()
fig.write_html("evolucao_redes_mascaras_anual.html")

4. Portugal¶

In [199]:
def analisar_portugal_por_periodo(networks_periodos, coords):

    portugal_stats = {}
    
    for periodo, G in networks_periodos.items():
        print(f"\n{'='*60}")
        print(f"PORTUGAL - {periodo.upper()}")
        print(f"{'='*60}")
        
        #validar portugal em todos os periodos
        if 'PRT' not in G.nodes():
            print(f"Portugal não encontrado na rede do período {periodo}")
            continue
            
        # estatísticas básicas 
        grau_total = G.degree('PRT')
        grau_entrada = G.in_degree('PRT') 
        grau_saida = G.out_degree('PRT')   
        
        print(f"Estatísticas Básicas:")
        print(f"  - Grau Total: {grau_total}")
        print(f"  - Exportações (grau saída): {grau_saida}")
        print(f"  - Importações (grau entrada): {grau_entrada}")
        
        # nodos com lig dorectas
        exportadores_portugal = list(G.predecessors('PRT'))  
        importadores_portugal = list(G.successors('PRT'))    
        
        print(f"\nParceiros Comerciais:")
        print(f"  - Exporta para {len(importadores_portugal)} países: {importadores_portugal}")
        print(f"  - Importa de {len(exportadores_portugal)} países: {exportadores_portugal}")
        
        # centralidades

        betweenness = nx.betweenness_centrality(G)['PRT']
        closeness = nx.closeness_centrality(G)['PRT'] 
        eigenvector = nx.eigenvector_centrality(G)['PRT']
        pagerank = nx.pagerank(G)['PRT']
            
        print(f"\nMedidas de Centralidade:")
        print(f"  - Betweenness: {betweenness:.4f}")
        print(f"  - Closeness: {closeness:.4f}")
        print(f"  - Eigenvector: {eigenvector:.4f}")
        print(f"  - PageRank: {pagerank:.4f}")

        
        # ranking de Portugal em grau
        all_degrees = dict(G.degree())
        portugal_rank = sorted(all_degrees.values(), reverse=True).index(grau_total) + 1
        total_paises = len(G.nodes())
        
        print(f"\nPosição Relativa:")
        print(f"  - Ranking por grau: {portugal_rank}º de {total_paises} países")
        print(f"  - Percentil: {((total_paises - portugal_rank) / total_paises * 100):.1f}%")
        
        # guardar stats
        portugal_stats[periodo] = {
            'grau_total': grau_total,
            'grau_entrada': grau_entrada,
            'grau_saida': grau_saida,
            'exportadores': exportadores_portugal,
            'importadores': importadores_portugal,
            'ranking': portugal_rank,
            'total_paises': total_paises
        }
    
    return portugal_stats


def comparar_evolucao_portugal(portugal_stats):

    print(f"\n{'='*60}")
    print("EVOLUÇÃO DE PORTUGAL ENTRE PERÍODOS")
    print(f"{'='*60}")
    
    periodos = list(portugal_stats.keys())
    
    df_portugal = pd.DataFrame(portugal_stats).T
    print("\nTabela Comparativa:")
    display(df_portugal[['grau_total', 'grau_entrada', 'grau_saida', 'ranking', 'total_paises']])
    
    # gráficos de evolução
    fig, axes = plt.subplots(2, 2, figsize=(15, 10))
    fig.suptitle("Evolução de Portugal nas Redes de Exportação de Máscaras", fontsize=16)
    
    #grau total
    axes[0, 0].plot(periodos, [portugal_stats[p]['grau_total'] for p in periodos], 'o-', color='blue')
    axes[0, 0].set_title('Grau Total')
    axes[0, 0].set_ylabel('Número de Conexões')
    axes[0, 0].grid(True, alpha=0.3)
    
    # export vs impor
    exportacoes = [portugal_stats[p]['grau_saida'] for p in periodos]
    importacoes = [portugal_stats[p]['grau_entrada'] for p in periodos]
    
    x = range(len(periodos))
    width = 0.35
    axes[0, 1].bar([i - width/2 for i in x], exportacoes, width, label='Exportações', color='green')
    axes[0, 1].bar([i + width/2 for i in x], importacoes, width, label='Importações', color='orange')
    axes[0, 1].set_title('Exportações vs Importações')
    axes[0, 1].set_ylabel('Número de Conexões')
    axes[0, 1].set_xticks(x)
    axes[0, 1].set_xticklabels(periodos)
    axes[0, 1].legend()
    axes[0, 1].grid(True, alpha=0.3)
    
    # Rank
    axes[1, 0].plot(periodos, [portugal_stats[p]['ranking'] for p in periodos], 'o-', color='red')
    axes[1, 0].set_title('Posição no Ranking')
    axes[1, 0].set_ylabel('Posição (menor = melhor)')
    axes[1, 0].invert_yaxis()  # Inverter para melhor visualização
    axes[1, 0].grid(True, alpha=0.3)
    
    # percentil
    percentis = [((portugal_stats[p]['total_paises'] - portugal_stats[p]['ranking']) / portugal_stats[p]['total_paises'] * 100) for p in periodos]
    axes[1, 1].plot(periodos, percentis, 'o-', color='purple')
    axes[1, 1].set_title('Percentil de Conectividade')
    axes[1, 1].set_ylabel('Percentil (%)')
    axes[1, 1].grid(True, alpha=0.3)
    
    plt.tight_layout()
    plt.show()
    
    # análise às mudanças
    print("\nAnálise de Mudanças:")
    for i in range(1, len(periodos)):
        periodo_anterior = periodos[i-1]
        periodo_atual = periodos[i]
        
        delta_grau = portugal_stats[periodo_atual]['grau_total'] - portugal_stats[periodo_anterior]['grau_total']
        delta_ranking = portugal_stats[periodo_atual]['ranking'] - portugal_stats[periodo_anterior]['ranking']
        
        print(f"\n{periodo_anterior} → {periodo_atual}:")
        print(f"  - Mudança no grau total: {delta_grau:+d}")
        print(f"  - Mudança no ranking: {delta_ranking:+d} posições")
        
        # novas ligações
        novos_exp = set(portugal_stats[periodo_atual]['exportadores']) - set(portugal_stats[periodo_anterior]['exportadores'])
        novos_imp = set(portugal_stats[periodo_atual]['importadores']) - set(portugal_stats[periodo_anterior]['importadores'])
        
        if novos_exp:
            print(f"  - Novos países que exportam para Portugal: {list(novos_exp)}")
        if novos_imp:
            print(f"  - Novos países que importam de Portugal: {list(novos_imp)}")

# Executar análise completa
print("O Caso de Portugal")
print()

# análise estatística por período
portugal_stats = analisar_portugal_por_periodo(networks_periodos, coords)


# comparação evolutiva
comparar_evolucao_portugal(portugal_stats)
O Caso de Portugal


============================================================
PORTUGAL - PRÉ-PANDEMIA
============================================================
Estatísticas Básicas:
  - Grau Total: 29
  - Exportações (grau saída): 15
  - Importações (grau entrada): 14

Parceiros Comerciais:
  - Exporta para 15 países: ['AGO', 'CHE', 'CZE', 'DEU', 'DNK', 'ESP', 'FRA', 'GBR', 'IRL', 'ITA', 'MAR', 'NLD', 'NOR', 'SWE', 'TUN']
  - Importa de 14 países: ['BEL', 'CHN', 'DEU', 'ESP', 'FRA', 'GBR', 'HKG', 'IND', 'ITA', 'MAR', 'NLD', 'ROU', 'TUR', 'VNM']

Medidas de Centralidade:
  - Betweenness: 0.0010
  - Closeness: 0.1977
  - Eigenvector: 0.1195
  - PageRank: 0.0072

Posição Relativa:
  - Ranking por grau: 23º de 159 países
  - Percentil: 85.5%

============================================================
PORTUGAL - DURANTE PANDEMIA
============================================================
Estatísticas Básicas:
  - Grau Total: 36
  - Exportações (grau saída): 19
  - Importações (grau entrada): 17

Parceiros Comerciais:
  - Exporta para 19 países: ['AGO', 'AUT', 'BEL', 'CHE', 'CPV', 'CZE', 'DEU', 'DNK', 'ESP', 'FRA', 'GBR', 'IRL', 'ITA', 'NLD', 'POL', 'ROU', 'SVK', 'SWE', 'USA']
  - Importa de 17 países: ['BEL', 'CHE', 'CHN', 'CZE', 'DEU', 'ESP', 'FRA', 'GBR', 'HKG', 'IND', 'ITA', 'MAR', 'NLD', 'ROU', 'SVN', 'TUR', 'USA']

Medidas de Centralidade:
  - Betweenness: 0.0021
  - Closeness: 0.2391
  - Eigenvector: 0.1393
  - PageRank: 0.0091

Posição Relativa:
  - Ranking por grau: 22º de 175 países
  - Percentil: 87.4%

============================================================
PORTUGAL - PÓS-PANDEMIA
============================================================
Estatísticas Básicas:
  - Grau Total: 40
  - Exportações (grau saída): 21
  - Importações (grau entrada): 19

Parceiros Comerciais:
  - Exporta para 21 países: ['AGO', 'BEL', 'BGR', 'CHE', 'CZE', 'DEU', 'DNK', 'DZA', 'ESP', 'FRA', 'GBR', 'IRL', 'ITA', 'MAR', 'NLD', 'NOR', 'POL', 'ROU', 'SVK', 'SWE', 'USA']
  - Importa de 19 países: ['BEL', 'CHN', 'CZE', 'DEU', 'DNK', 'ESP', 'FRA', 'GBR', 'GRC', 'ITA', 'MAR', 'NLD', 'POL', 'ROU', 'SVN', 'SWE', 'TUR', 'UKR', 'USA']

Medidas de Centralidade:
  - Betweenness: 0.0020
  - Closeness: 0.2396
  - Eigenvector: 0.1499
  - PageRank: 0.0091

Posição Relativa:
  - Ranking por grau: 21º de 167 países
  - Percentil: 87.4%

============================================================
EVOLUÇÃO DE PORTUGAL ENTRE PERÍODOS
============================================================

Tabela Comparativa:
grau_total grau_entrada grau_saida ranking total_paises
Pré-Pandemia 29 14 15 23 159
Durante Pandemia 36 17 19 22 175
Pós-Pandemia 40 19 21 21 167
No description has been provided for this image
Análise de Mudanças:

Pré-Pandemia → Durante Pandemia:
  - Mudança no grau total: +7
  - Mudança no ranking: -1 posições
  - Novos países que exportam para Portugal: ['USA', 'SVN', 'CZE', 'CHE']
  - Novos países que importam de Portugal: ['SVK', 'CPV', 'AUT', 'BEL', 'USA', 'POL', 'ROU']

Durante Pandemia → Pós-Pandemia:
  - Mudança no grau total: +4
  - Mudança no ranking: -1 posições
  - Novos países que exportam para Portugal: ['GRC', 'UKR', 'DNK', 'SWE', 'POL']
  - Novos países que importam de Portugal: ['DZA', 'MAR', 'BGR', 'NOR']
In [ ]: